<script setup>
import { onMounted, ref, toRaw } from 'vue'
import { storeToRefs } from 'pinia'
import { ElMessage } from 'element-plus'
import CodeMirror from 'codemirror'

import fileApi from '@/utils/file'
import { useStore } from '@/stores'

import EditorHeader from '@/components/CodemirrorEditor/EditorHeader/index.vue'
import InsertFormDialog from '@/components/CodemirrorEditor/InsertFormDialog.vue'
import UploadImgDialog from '@/components/CodemirrorEditor/UploadImgDialog.vue'
import CssEditor from '@/components/CodemirrorEditor/CssEditor.vue'
import RunLoading from '@/components/RunLoading.vue'

import {
  ContextMenu,
  ContextMenuContent,
  ContextMenuItem,
  ContextMenuSeparator,
  ContextMenuShortcut,
  ContextMenuTrigger,
} from '@/components/ui/context-menu'

import { altKey, altSign, ctrlKey, shiftKey, shiftSign } from '@/config'

import {
  checkImage,
  formatDoc,
  toBase64,
} from '@/utils'

import 'codemirror/mode/javascript/javascript'

const store = useStore()
const { output, editor, editorContent, isShowCssEditor } = storeToRefs(store)

const {
  editorRefresh,
  exportEditorContent2HTML,
  exportEditorContent2MD,
  formatContent,
  importMarkdownContent,
  resetStyleConfirm,
  toggleShowInsertFormDialog,
  toggleShowUploadImgDialog,
} = store

const isImgLoading = ref(false)
const timeout = ref(0)

const preview = ref(null)

// 使浏览区与编辑区滚动条建立同步联系
function leftAndRightScroll() {
  const scrollCB = (text) => {
    let source, target

    clearTimeout(timeout.value)
    if (text === `preview`) {
      source = preview.value.$el
      target = document.querySelector(`.CodeMirror-scroll`)

      editor.value.off(`scroll`, editorScrollCB)
      timeout.value = setTimeout(() => {
        editor.value.on(`scroll`, editorScrollCB)
      }, 300)
    }
    else if (text === `editor`) {
      source = document.querySelector(`.CodeMirror-scroll`)
      target = preview.value.$el

      target.removeEventListener(`scroll`, previewScrollCB, false)
      timeout.value = setTimeout(() => {
        target.addEventListener(`scroll`, previewScrollCB, false)
      }, 300)
    }

    const percentage
          = source.scrollTop / (source.scrollHeight - source.offsetHeight)
    const height = percentage * (target.scrollHeight - target.offsetHeight)

    target.scrollTo(0, height)
  }

  function editorScrollCB() {
    scrollCB(`editor`)
  }

  function previewScrollCB() {
    scrollCB(`preview`)
  }

  preview.value.$el.addEventListener(`scroll`, previewScrollCB, false)
  editor.value.on(`scroll`, editorScrollCB)
}

onMounted(() => {
  setTimeout(() => {
    leftAndRightScroll()
  }, 300)
})

// 更新编辑器
function onEditorRefresh() {
  editorRefresh()
}

const backLight = ref(false)
const isCoping = ref(false)

function startCopy() {
  isCoping.value = true
  backLight.value = true
}

// 拷贝结束
function endCopy() {
  backLight.value = false
  setTimeout(() => {
    isCoping.value = false
  }, 800)
}

function beforeUpload(file) {
  // validate image
  const checkResult = checkImage(file)
  if (!checkResult.ok) {
    ElMessage.error(checkResult.msg)
    return false
  }

  // check image host
  const imgHost = localStorage.getItem(`imgHost`) || `default`
  localStorage.setItem(`imgHost`, imgHost)

  const config = localStorage.getItem(`${imgHost}Config`)
  const isValidHost = imgHost === `default` || config
  if (!isValidHost) {
    ElMessage.error(`请先配置 ${imgHost} 图床参数`)
    return false
  }
  return true
}

// 图片上传结束
function uploaded(imageUrl) {
  if (!imageUrl) {
    ElMessage.error(`上传图片未知异常`)
    return
  }
  toggleShowUploadImgDialog(false)
  // 上传成功，获取光标
  const cursor = editor.value.getCursor()
  const markdownImage = `![](${imageUrl})`
  // 将 Markdown 形式的 URL 插入编辑框光标所在位置
  toRaw(store.editor).replaceSelection(`\n${markdownImage}\n`, cursor)
  ElMessage.success(`图片上传成功`)
}
function uploadImage(file, cb) {
  isImgLoading.value = true

  toBase64(file)
    .then(base64Content => fileApi.fileUpload(base64Content, file))
    .then((url) => {
      if (cb) {
        cb(url)
      }
      else {
        uploaded(url)
      }
    })
    .catch((err) => {
      ElMessage.error(err.message)
    })
    .finally(() => {
      isImgLoading.value = false
    })
}

const changeTimer = ref(0)

// 初始化编辑器
function initEditor() {
  const editorDom = document.querySelector(`#editor`)

  if (!editorDom.value) {
    editorDom.value = editorContent.value
  }
  editor.value = CodeMirror.fromTextArea(editorDom, {
    mode: `text/x-markdown`,
    theme: `xq-light`,
    lineNumbers: false,
    lineWrapping: true,
    styleActiveLine: true,
    autoCloseBrackets: true,
    extraKeys: {
      [`${shiftKey}-${altKey}-F`]: function autoFormat(editor) {
        formatDoc(editor.getValue(0)).then((doc) => {
          editor.setValue(doc)
        })
      },
      [`${ctrlKey}-B`]: function bold(editor) {
        const selected = editor.getSelection()
        editor.replaceSelection(`**${selected}**`)
      },
      [`${ctrlKey}-I`]: function italic(editor) {
        const selected = editor.getSelection()
        editor.replaceSelection(`*${selected}*`)
      },
      [`${ctrlKey}-D`]: function del(editor) {
        const selected = editor.getSelection()
        editor.replaceSelection(`~~${selected}~~`)
      },
      [`${ctrlKey}-K`]: function italic(editor) {
        const selected = editor.getSelection()
        editor.replaceSelection(`[${selected}]()`)
      },
      [`${ctrlKey}-E`]: function code(editor) {
        const selected = editor.getSelection()
        editor.replaceSelection(`\`${selected}\``)
      },
      // 预备弃用
      [`${ctrlKey}-L`]: function code(editor) {
        const selected = editor.getSelection()
        editor.replaceSelection(`\`${selected}\``)
      },
    },
  })

  editor.value.on(`change`, (e) => {
    clearTimeout(changeTimer.value)
    changeTimer.value = setTimeout(() => {
      onEditorRefresh()
      editorContent.value = e.getValue()
    }, 300)
  })

  // 粘贴上传图片并插入
  editor.value.on(`paste`, (cm, e) => {
    if (!(e.clipboardData && e.clipboardData.items) || isImgLoading.value) {
      return
    }
    for (let i = 0, len = e.clipboardData.items.length; i < len; ++i) {
      const item = e.clipboardData.items[i]
      if (item.kind === `file`) {
        // 校验图床参数
        const pasteFile = item.getAsFile()
        const isValid = beforeUpload(pasteFile)
        if (!isValid) {
          continue
        }
        uploadImage(pasteFile)
      }
    }
  })
}

const container = ref(null)

// 工具函数，添加格式
function addFormat(cmd) {
  editor.value.options.extraKeys[cmd](editor.value)
}

const codeMirrorWrapper = ref(null)

// 转换 markdown 中的本地图片为线上图片
// todo 处理事件覆盖
function mdLocalToRemote() {
  const dom = codeMirrorWrapper.value.$el

  // 上传 md 中的图片
  const uploadMdImg = async ({ md, list }) => {
    const mdImgList = [
      ...(md.str.matchAll(/!\[(.*?)\]\((.*?)\)/g) || []),
    ].filter((item) => {
      return item // 获取所有相对地址的图片
    })
    const root = md.path.match(/.+?\//)[0]
    const resList = await Promise.all(
      mdImgList.map((item) => {
        return new Promise((resolve) => {
          let [, , matchStr] = item
          matchStr = matchStr.replace(/^.\//, ``) // 处理 ./img/ 为 img/ 统一相对路径风格
          const { file }
                = list.find(f => f.path === `${root}${matchStr}`) || {}
          uploadImage(file, (url) => {
            resolve({ matchStr, url })
          })
        })
      }),
    )
    resList.forEach((item) => {
      md.str = md.str
        .replace(`](./${item.matchStr})`, `](${item.url})`)
        .replace(`](${item.matchStr})`, `](${item.url})`)
    })
    editor.value.setValue(md.str)
  }

  dom.ondragover = evt => evt.preventDefault()
  dom.ondrop = async (evt) => {
    evt.preventDefault()
    for (const item of evt.dataTransfer.items) {
      item.getAsFileSystemHandle().then(async (handle) => {
        if (handle.kind === `directory`) {
          const list = await showFileStructure(handle)
          const md = await getMd({ list })
          uploadMdImg({ md, list })
        }
        else {
          const file = await handle.getFile()
          console.log(`file`, file)
        }
      })
    }
  }

  // 从文件列表中查找一个 md 文件并解析
  async function getMd({ list }) {
    return new Promise((resolve) => {
      const { path, file } = list.find(item => item.path.match(/\.md$/))
      const reader = new FileReader()
      reader.readAsText(file, `UTF-8`)
      reader.onload = (evt) => {
        resolve({
          str: evt.target.result,
          file,
          path,
        })
      }
    })
  }

  // 转换文件系统句柄中的文件为文件列表
  async function showFileStructure(root) {
    const result = []
    let cwd = ``
    try {
      const dirs = [root]
      for (const dir of dirs) {
        cwd += `${dir.name}/`
        for await (const [, handle] of dir) {
          if (handle.kind === `file`) {
            result.push({
              path: cwd + handle.name,
              file: await handle.getFile(),
            })
          }
          else {
            result.push({
              path: `${cwd + handle.name}/`,
            })
            dirs.push(handle)
          }
        }
      }
    }
    catch (err) {
      console.error(err)
    }
    return result
  }
}

onMounted(() => {
  initEditor()
  onEditorRefresh()
  mdLocalToRemote()
})
</script>

<template>
  <div ref="container" class="container">
    <el-container>
      <el-header class="editor__header">
        <EditorHeader
          @add-format="addFormat"
          @format-content="formatContent"
          @start-copy="startCopy"
          @end-copy="endCopy"
        />
      </el-header>
      <el-main class="container-main">
        <el-row class="container-main-section">
          <el-col
            ref="codeMirrorWrapper"
            :span="isShowCssEditor ? 8 : 12"
            class="codeMirror-wrapper"
            :class="{
              'order-1': !store.isEditOnLeft,
            }"
          >
            <ContextMenu>
              <ContextMenuTrigger>
                <textarea
                  id="editor"
                  type="textarea"
                  placeholder="Your markdown text here."
                />
              </ContextMenuTrigger>
              <ContextMenuContent class="w-64">
                <ContextMenuItem inset @click="toggleShowUploadImgDialog()">
                  上传图片
                </ContextMenuItem>
                <ContextMenuItem inset @click="toggleShowInsertFormDialog()">
                  插入表格
                </ContextMenuItem>
                <ContextMenuItem inset @click="resetStyleConfirm()">
                  恢复默认样式
                </ContextMenuItem>
                <ContextMenuSeparator />
                <ContextMenuItem inset @click="importMarkdownContent()">
                  导入 .md 文档
                </ContextMenuItem>
                <ContextMenuItem inset @click="exportEditorContent2MD()">
                  导出 .md 文档
                </ContextMenuItem>
                <ContextMenuItem inset @click="exportEditorContent2HTML()">
                  导出 .html
                </ContextMenuItem>
                <ContextMenuItem inset @click="formatContent()">
                  格式化
                  <ContextMenuShortcut>{{ altSign }} + {{ shiftSign }} + F</ContextMenuShortcut>
                </ContextMenuItem>
              </ContextMenuContent>
            </ContextMenu>
          </el-col>
          <el-col
            id="preview"
            ref="preview"
            :span="isShowCssEditor ? 8 : 12"
            class="preview-wrapper"
          >
            <div id="output-wrapper" :class="{ output_night: !backLight }">
              <div class="preview">
                <section id="output" v-html="output" />
                <div v-if="isCoping" class="loading-mask">
                  <div class="loading-mask-box">
                    <div class="loading__img" />
                    <span>正在生成</span>
                  </div>
                </div>
              </div>
            </div>
          </el-col>
          <CssEditor />
        </el-row>
      </el-main>
    </el-container>

    <UploadImgDialog
      @before-upload="beforeUpload"
      @upload-image="uploadImage"
      @uploaded="uploaded"
    />

    <InsertFormDialog />

    <RunLoading />
  </div>
</template>

<style lang="less" scoped>
@import url('../assets/less/app.less');
</style>

<style lang="less" scoped>
.container {
  height: 100%;
  min-width: 100%;
  padding: 0;
}

.container-main {
  padding: 20px;
  padding-top: 0;
}

.container-main-section {
  height: 100%;
}

#output-wrapper {
  position: relative;
  user-select: text;
  height: 100%;
}

.loading-mask {
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  text-align: center;
  color: var(--el-text-color-regular);
  background-color: var(--el-bg-color);

  .loading-mask-box {
    position: sticky;
    top: 50%;
    transform: translateY(-50%);

    .loading__img {
      width: 75px;
      height: 75px;
      background: url('../assets/images/favicon.png') no-repeat;
      margin: 1em auto;
      background-size: cover;
    }
  }
}

:deep(.preview-table) {
  border-spacing: 0;
}

.codeMirror-wrapper,
.preview-wrapper {
  height: 100%;
}

.codeMirror-wrapper {
  overflow-x: auto;
}
</style>
